﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the Microsoft Public License (Ms-PL).
// See accompanying file License.txt or copy at http://mews.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Mews.IO
{
    using System;
    using System.Diagnostics.CodeAnalysis;
    using System.IO;

    /// <summary>Transparently fixes line endings in the underlying stream.</summary>
    /// <para><b>Caution</b>: When data is written to this stream, then <see cref="Stream.Dispose()"/> <b>must</b> be
    /// called in the end, see <see cref="Dispose"/> for more information.</para>
    /// <threadsafety static="true" instance="false"/>
    public sealed class FixLineEndingsStream : BufferStream
    {
        private bool lastReadWasCarriageReturn;
        private bool lastWrittenWasCarriageReturn;

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>Initializes a new instance of the <see cref="FixLineEndingsStream"/> class by calling
        /// <see cref="FixLineEndingsStream(Stream, int)"/>(<paramref name="stream"/>, 1024).</summary>
        public FixLineEndingsStream(Stream stream) : this(stream, Settings.InMemoryStreamBufferSize)
        {
        }

        /// <summary>Initializes a new instance of the <see cref="FixLineEndingsStream"/> class.</summary>
        /// <param name="stream">The stream from/to which encoded data is read/written.</param>
        /// <param name="bufferSize">The size of the internal read and write buffers in bytes.</param>
        /// <exception cref="ArgumentNullException"><paramref name="stream"/> is <c>null</c>.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="bufferSize"/> is less than or equal to 0.
        /// </exception>
        public FixLineEndingsStream(Stream stream, int bufferSize) : base(stream, bufferSize)
        {
        }

        /// <summary>See <see cref="Stream.Read(byte[], int, int)"/>.</summary>
        public sealed override int Read(byte[] buffer, int offset, int count)
        {
            this.AssertNotDisposed();
            BufferHelper.AssertValidRange(buffer, "buffer", offset, "offset", count, "count");

            // Letting the code run as unchecked leads to better performance in the device emulator. On the desktop
            // performance does not seem to be affected.
            unchecked
            {
                ReadBuffer readBuffer = this.ReadBuffer;
                int index = offset;
                int pastEnd = offset + count;
                byte currentByte;

                // Only try to get more original data if we haven't yet copied anything into buffer
                while ((index < pastEnd) && ((readBuffer.Index < readBuffer.Count) ||
                    ((index == offset) && this.IsMoreOriginalDataAvailable(readBuffer))))
                {
                    currentByte = readBuffer[readBuffer.Index];

                    if (this.lastReadWasCarriageReturn)
                    {
                        if (currentByte == (byte)'\n')
                        {
                            ++readBuffer.Index;
                        }
                        else
                        {
                            currentByte = (byte)'\n';
                        }

                        this.lastReadWasCarriageReturn = false;
                    }
                    else
                    {
                        switch (currentByte)
                        {
                            case (byte)'\r':
                                ++readBuffer.Index;
                                this.lastReadWasCarriageReturn = true;
                                break;
                            case (byte)'\n':
                                currentByte = (byte)'\r';
                                this.lastReadWasCarriageReturn = true;
                                break;
                            default:
                                ++readBuffer.Index;
                                break;
                        }
                    }

                    buffer[index++] = currentByte;
                }

                return index - offset;
            }
        }

        /// <summary>See <see cref="Stream.Write(byte[], int, int)"/>.</summary>
        public sealed override void Write(byte[] buffer, int offset, int count)
        {
            this.AssertNotDisposed();
            BufferHelper.AssertValidRange(buffer, "buffer", offset, "offset", count, "count");

            unchecked
            {
                WriteBuffer writeBuffer = this.WriteBuffer;
                int pastEnd = offset + count;
                byte currentByte;

                while ((offset < pastEnd) && ((writeBuffer.Count < writeBuffer.Capacity) || writeBuffer.Flush()))
                {
                    currentByte = buffer[offset];

                    if (this.lastWrittenWasCarriageReturn)
                    {
                        if (currentByte == (byte)'\n')
                        {
                            ++offset;
                        }
                        else
                        {
                            currentByte = (byte)'\n';
                        }

                        this.lastWrittenWasCarriageReturn = false;
                    }
                    else
                    {
                        switch (currentByte)
                        {
                            case (byte)'\r':
                                ++offset;
                                this.lastWrittenWasCarriageReturn = true;
                                break;
                            case (byte)'\n':
                                currentByte = (byte)'\r';
                                this.lastWrittenWasCarriageReturn = true;
                                break;
                            default:
                                ++offset;
                                break;
                        }
                    }

                    writeBuffer[writeBuffer.Count++] = currentByte;
                }
            }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        internal FixLineEndingsStream(ReadBuffer readBuffer, WriteBuffer writeBuffer) : base(readBuffer, writeBuffer)
        {
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>See <see cref="Stream.Dispose(bool)"/>.</summary>
        /// <remarks>If <paramref name="disposing"/> is <c>true</c> then possibly remaining bytes are written into the
        /// underlying stream.</remarks>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Dispose() must never throw.")]
        protected sealed override void Dispose(bool disposing)
        {
            try
            {
                if (disposing && !this.IsDisposed && this.lastWrittenWasCarriageReturn)
                {
                    var writeBuffer = this.WriteBuffer;

                    if ((writeBuffer.Count < writeBuffer.Capacity) || writeBuffer.Flush())
                    {
                        writeBuffer[writeBuffer.Count++] = (byte)'\n';
                    }
                }
            }
            catch
            {
            }
            finally
            {
                base.Dispose(disposing);
            }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private bool IsMoreOriginalDataAvailable(ReadBuffer readBuffer)
        {
            if (readBuffer.Fill())
            {
                return true;
            }

            if (this.lastReadWasCarriageReturn)
            {
                // We're at the end of the original stream. If the last byte was a CR then we need to add a LF.
                readBuffer[0] = (byte)'\n';
                readBuffer.Index = 0;
                readBuffer.Count = 1;
            }

            return this.lastReadWasCarriageReturn;
        }
    }
}
